/*
 * Copyright (C) 2012-2016 Markus Junginger, greenrobot (http://greenrobot.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.greenrobot.eventbus;

import org.greenrobot.eventbus.meta.SubscriberInfo;
import org.greenrobot.eventbus.meta.SubscriberInfoIndex;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

class SubscriberMethodFinder {
  /*
   * In newer class files, compilers may add methods. Those are called bridge or synthetic methods.
   * EventBus must ignore both. There modifiers are not public but defined in the Java class file format:
   * http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.6-200-A.1
   */
  private static final int BRIDGE = 0x40;
  private static final int SYNTHETIC = 0x1000;

  private static final int MODIFIERS_IGNORE =
      Modifier.ABSTRACT | Modifier.STATIC | BRIDGE | SYNTHETIC;
  private static final Map<Class<?>, List<SubscriberMethod>> METHOD_CACHE =
      new ConcurrentHashMap<>();

  private List<SubscriberInfoIndex> subscriberInfoIndexes;
  private final boolean strictMethodVerification;
  private final boolean ignoreGeneratedIndex;

  private static final int POOL_SIZE = 4;
  private static final FindState[] FIND_STATE_POOL = new FindState[POOL_SIZE];

  SubscriberMethodFinder(
      List<SubscriberInfoIndex> subscriberInfoIndexes,
      boolean strictMethodVerification,
      boolean ignoreGeneratedIndex) {
    this.subscriberInfoIndexes = subscriberInfoIndexes;
    this.strictMethodVerification = strictMethodVerification;
    this.ignoreGeneratedIndex = ignoreGeneratedIndex;
  }

  List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
    if (subscriberMethods != null) {
      return subscriberMethods;
    }

    if (ignoreGeneratedIndex) {
      subscriberMethods = findUsingReflection(subscriberClass);
    } else {
      subscriberMethods = findUsingInfo(subscriberClass);
    }
    if (subscriberMethods.isEmpty()) {
      throw new EventBusException(
          "Subscriber "
              + subscriberClass
              + " and its super classes have no public methods with the @Subscribe annotation");
    } else {
      METHOD_CACHE.put(subscriberClass, subscriberMethods);
      return subscriberMethods;
    }
  }

  private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
    FindState findState = prepareFindState();
    findState.initForSubscriber(subscriberClass);
    while (findState.clazz != null) {
      findState.subscriberInfo = getSubscriberInfo(findState);
      if (findState.subscriberInfo != null) {
        SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
        for (SubscriberMethod subscriberMethod : array) {
          if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
            findState.subscriberMethods.add(subscriberMethod);
          }
        }
      } else {
        findUsingReflectionInSingleClass(findState);
      }
      findState.moveToSuperclass();
    }
    return getMethodsAndRelease(findState);
  }

  private List<SubscriberMethod> getMethodsAndRelease(FindState findState) {
    List<SubscriberMethod> subscriberMethods = new ArrayList<>(findState.subscriberMethods);
    findState.recycle();
    synchronized (FIND_STATE_POOL) {
      for (int i = 0; i < POOL_SIZE; i++) {
        if (FIND_STATE_POOL[i] == null) {
          FIND_STATE_POOL[i] = findState;
          break;
        }
      }
    }
    return subscriberMethods;
  }

  private FindState prepareFindState() {
    synchronized (FIND_STATE_POOL) {
      for (int i = 0; i < POOL_SIZE; i++) {
        FindState state = FIND_STATE_POOL[i];
        if (state != null) {
          FIND_STATE_POOL[i] = null;
          return state;
        }
      }
    }
    return new FindState();
  }

  private SubscriberInfo getSubscriberInfo(FindState findState) {
    if (findState.subscriberInfo != null
        && findState.subscriberInfo.getSuperSubscriberInfo() != null) {
      SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo();
      if (findState.clazz == superclassInfo.getSubscriberClass()) {
        return superclassInfo;
      }
    }
    if (subscriberInfoIndexes != null) {
      for (SubscriberInfoIndex index : subscriberInfoIndexes) {
        SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
        if (info != null) {
          return info;
        }
      }
    }
    return null;
  }

  private List<SubscriberMethod> findUsingReflection(Class<?> subscriberClass) {
    FindState findState = prepareFindState();
    findState.initForSubscriber(subscriberClass);
    while (findState.clazz != null) {
      findUsingReflectionInSingleClass(findState);
      findState.moveToSuperclass();
    }
    return getMethodsAndRelease(findState);
  }

  private void findUsingReflectionInSingleClass(FindState findState) {
    Method[] methods;
    try {
      // This is faster than getMethods, especially when subscribers are fat classes like Activities
      methods = findState.clazz.getDeclaredMethods();
    } catch (Throwable th) {
      // Workaround for java.lang.NoClassDefFoundError, see
      // https://github.com/greenrobot/EventBus/issues/149
      try {
        methods = findState.clazz.getMethods();
      } catch (
          LinkageError error) { // super class of NoClassDefFoundError to be a bit more broad...
        String msg = "Could not inspect methods of " + findState.clazz.getName();
        if (ignoreGeneratedIndex) {
          msg += ". Please consider using EventBus annotation processor to avoid reflection.";
        } else {
          msg +=
              ". Please make this class visible to EventBus annotation processor to avoid reflection.";
        }
        throw new EventBusException(msg, error);
      }
      findState.skipSuperClasses = true;
    }
    for (Method method : methods) {
      int modifiers = method.getModifiers();
      if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (parameterTypes.length == 1) {
          Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
          if (subscribeAnnotation != null) {
            Class<?> eventType = parameterTypes[0];
            if (findState.checkAdd(method, eventType)) {
              ThreadMode threadMode = subscribeAnnotation.threadMode();
              findState.subscriberMethods.add(
                  new SubscriberMethod(
                      method,
                      eventType,
                      threadMode,
                      subscribeAnnotation.priority(),
                      subscribeAnnotation.sticky()));
            }
          }
        } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
          String methodName = method.getDeclaringClass().getName() + "." + method.getName();
          throw new EventBusException(
              "@Subscribe method "
                  + methodName
                  + "must have exactly 1 parameter but has "
                  + parameterTypes.length);
        }
      } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
        String methodName = method.getDeclaringClass().getName() + "." + method.getName();
        throw new EventBusException(
            methodName
                + " is a illegal @Subscribe method: must be public, non-static, and non-abstract");
      }
    }
  }

  static void clearCaches() {
    METHOD_CACHE.clear();
  }

  static class FindState {
    final List<SubscriberMethod> subscriberMethods = new ArrayList<>();
    final Map<Class, Object> anyMethodByEventType = new HashMap<>();
    final Map<String, Class> subscriberClassByMethodKey = new HashMap<>();
    final StringBuilder methodKeyBuilder = new StringBuilder(128);

    Class<?> subscriberClass;
    Class<?> clazz;
    boolean skipSuperClasses;
    SubscriberInfo subscriberInfo;

    void initForSubscriber(Class<?> subscriberClass) {
      this.subscriberClass = clazz = subscriberClass;
      skipSuperClasses = false;
      subscriberInfo = null;
    }

    void recycle() {
      subscriberMethods.clear();
      anyMethodByEventType.clear();
      subscriberClassByMethodKey.clear();
      methodKeyBuilder.setLength(0);
      subscriberClass = null;
      clazz = null;
      skipSuperClasses = false;
      subscriberInfo = null;
    }

    boolean checkAdd(Method method, Class<?> eventType) {
      // 2 level check: 1st level with event type only (fast), 2nd level with complete signature
      // when required.
      // Usually a subscriber doesn't have methods listening to the same event type.
      Object existing = anyMethodByEventType.put(eventType, method);
      if (existing == null) {
        return true;
      } else {
        if (existing instanceof Method) {
          if (!checkAddWithMethodSignature((Method) existing, eventType)) {
            // Paranoia check
            throw new IllegalStateException();
          }
          // Put any non-Method object to "consume" the existing Method
          anyMethodByEventType.put(eventType, this);
        }
        return checkAddWithMethodSignature(method, eventType);
      }
    }

    private boolean checkAddWithMethodSignature(Method method, Class<?> eventType) {
      methodKeyBuilder.setLength(0);
      methodKeyBuilder.append(method.getName());
      methodKeyBuilder.append('>').append(eventType.getName());

      String methodKey = methodKeyBuilder.toString();
      Class<?> methodClass = method.getDeclaringClass();
      Class<?> methodClassOld = subscriberClassByMethodKey.put(methodKey, methodClass);
      if (methodClassOld == null || methodClassOld.isAssignableFrom(methodClass)) {
        // Only add if not already found in a sub class
        return true;
      } else {
        // Revert the put, old class is further down the class hierarchy
        subscriberClassByMethodKey.put(methodKey, methodClassOld);
        return false;
      }
    }

    void moveToSuperclass() {
      if (skipSuperClasses) {
        clazz = null;
      } else {
        clazz = clazz.getSuperclass();
        String clazzName = clazz.getName();
        // Skip system classes, this degrades performance.
        // Also we might avoid some ClassNotFoundException (see FAQ for background).
        if (clazzName.startsWith("java.")
            || clazzName.startsWith("javax.")
            || clazzName.startsWith("android.")
            || clazzName.startsWith("androidx.")
            || clazzName.startsWith("jdkx.")
            || clazzName.startsWith("openjdk.")) {
          clazz = null;
        }
      }
    }
  }
}
